Micron Document
🎖️GitЯра🎖️

Node / meshtastic / Meshtastic-Android / files / core / service / src / commonMain / kotlin / org / meshtastic / core / service / SharedRadioInterfaceService.kt

Displaying Raw • Download

core/service/src/commonMain/kotlin/org/meshtastic/core/service/SharedRadioInterfaceService.kt e85300531e124df08788c1b765d50af1bf6d516d (e8530053) Text, 14.98 KB

T8b949e/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
Tff7b72package T7ee787org.meshtastic.core.service

Tff7b72import T7ee787androidx.lifecycle.Lifecycle
Tff7b72import T7ee787androidx.lifecycle.coroutineScope
Tff7b72import T7ee787co.touchlab.kermit.Logger
Tff7b72import T7ee787kotlinx.atomicfu.atomic
Tff7b72import T7ee787kotlinx.coroutines.CoroutineScope
Tff7b72import T7ee787kotlinx.coroutines.Job
Tff7b72import T7ee787kotlinx.coroutines.SupervisorJob
Tff7b72import T7ee787kotlinx.coroutines.cancel
Tff7b72import T7ee787kotlinx.coroutines.channels.BufferOverflow
Tff7b72import T7ee787kotlinx.coroutines.delay
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableSharedFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.MutableStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.SharedFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.StateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.asSharedFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.asStateFlow
Tff7b72import T7ee787kotlinx.coroutines.flow.catch
Tff7b72import T7ee787kotlinx.coroutines.flow.launchIn
Tff7b72import T7ee787kotlinx.coroutines.flow.onEach
Tff7b72import T7ee787kotlinx.coroutines.launch
Tff7b72import T7ee787kotlinx.coroutines.sync.Mutex
Tff7b72import T7ee787kotlinx.coroutines.sync.withLock
Tff7b72import T7ee787org.koin.core.annotation.Named
Tff7b72import T7ee787org.koin.core.annotation.Single
Tff7b72import T7ee787org.meshtastic.core.ble.BluetoothRepository
Tff7b72import T7ee787org.meshtastic.core.common.util.handledLaunch
Tff7b72import T7ee787org.meshtastic.core.common.util.ignoreException
Tff7b72import T7ee787org.meshtastic.core.common.util.nowMillis
Tff7b72import T7ee787org.meshtastic.core.di.CoroutineDispatchers
Tff7b72import T7ee787org.meshtastic.core.model.ConnectionState
Tff7b72import T7ee787org.meshtastic.core.model.DeviceType
Tff7b72import T7ee787org.meshtastic.core.model.InterfaceId
Tff7b72import T7ee787org.meshtastic.core.model.MeshActivity
Tff7b72import T7ee787org.meshtastic.core.model.util.anonymize
Tff7b72import T7ee787org.meshtastic.core.network.repository.NetworkRepository
Tff7b72import T7ee787org.meshtastic.core.repository.PlatformAnalytics
Tff7b72import T7ee787org.meshtastic.core.repository.RadioInterfaceService
Tff7b72import T7ee787org.meshtastic.core.repository.RadioPrefs
Tff7b72import T7ee787org.meshtastic.core.repository.RadioTransport
Tff7b72import T7ee787org.meshtastic.core.repository.RadioTransportFactory
Tff7b72import T7ee787kotlin.concurrent.Volatile

T8b949e/**
* Shared multiplatform connection orchestrator for Meshtastic radios.
*
* Manages the connection lifecycle (connect, active, disconnect, reconnect loop), device address state flows, and
* hardware state observability (BLE/Network toggles). Delegates the actual raw byte transport mapping to a
* platform-specific [RadioTransportFactory].
*/
Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffLongParameterListTa5d6ff"Tb4b4b4, Ta5d6ff"Ta5d6ffTooManyFunctionsTa5d6ff"Tb4b4b4)
Tf0883e@Single
Tff7b72class T56d364SharedRadioInterfaceServiceTb4b4b4(
Tff7b72private Tff7b72val Te6edf3dispatchersTb4b4b4: Te6edf3CoroutineDispatchersTb4b4b4,
Tff7b72private Tff7b72val Te6edf3bluetoothRepositoryTb4b4b4: Te6edf3BluetoothRepositoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3networkRepositoryTb4b4b4: Te6edf3NetworkRepositoryTb4b4b4,
Tf0883e@NamedTb4b4b4(Ta5d6ff"Ta5d6ffProcessLifecycleTa5d6ff"Tb4b4b4) Tff7b72private Tff7b72val Te6edf3processLifecycleTb4b4b4: Te6edf3LifecycleTb4b4b4,
Tff7b72private Tff7b72val Te6edf3radioPrefsTb4b4b4: Te6edf3RadioPrefsTb4b4b4,
Tff7b72private Tff7b72val Te6edf3transportFactoryTb4b4b4: Te6edf3RadioTransportFactoryTb4b4b4,
Tff7b72private Tff7b72val Te6edf3analyticsTb4b4b4: Te6edf3PlatformAnalyticsTb4b4b4,
Tb4b4b4) Tb4b4b4: Te6edf3RadioInterfaceService Tb4b4b4{

Tff7b72override Tff7b72val Te6edf3supportedDeviceTypesTb4b4b4: Te6edf3ListTff7b72<Te6edf3DeviceTypeTff7b72>
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3transportFactoryTb4b4b4.Te6edf3supportedDeviceTypes

T8b949e/**
* Transport-level connection state reflecting the raw hardware link status.
*
* Updated directly by [onConnect] and [onDisconnect] when the physical transport (BLE, TCP, Serial) connects or
* disconnects. This is consumed exclusively by
* [MeshConnectionManager][org.meshtastic.core.repository.MeshConnectionManager], which reconciles it into the
* canonical app-level
* [ServiceRepository.connectionState][org.meshtastic.core.repository.ServiceRepository.connectionState].
*/
Tff7b72private Tff7b72val Te6edf3_connectionState Tff7b72= Te6edf3MutableStateFlowTff7b72<Te6edf3ConnectionStateTff7b72>Tb4b4b4(Te6edf3ConnectionStateTb4b4b4.Te6edf3DisconnectedTb4b4b4)
Tff7b72override Tff7b72val Te6edf3connectionStateTb4b4b4: Te6edf3StateFlowTff7b72<Te6edf3ConnectionStateTff7b72> Tff7b72= Te6edf3_connectionStateTb4b4b4.Te6edf3asStateFlowTb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3_currentDeviceAddressFlow Tff7b72= Te6edf3MutableStateFlowTff7b72<Tffa657String?Tff7b72>Tb4b4b4(Te6edf3radioPrefsTb4b4b4.Te6edf3devAddrTb4b4b4.Te6edf3valueTb4b4b4)
Tff7b72override Tff7b72val Te6edf3currentDeviceAddressFlowTb4b4b4: Te6edf3StateFlowTff7b72<Tffa657String?Tff7b72> Tff7b72= Te6edf3_currentDeviceAddressFlowTb4b4b4.Te6edf3asStateFlowTb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3_receivedData Tff7b72= Te6edf3MutableSharedFlowTff7b72<Te6edf3ByteArrayTff7b72>Tb4b4b4(Te6edf3extraBufferCapacity Tff7b72= T79c0ff6T79c0ff4Tb4b4b4)
Tff7b72override Tff7b72val Te6edf3receivedDataTb4b4b4: Te6edf3SharedFlowTff7b72<Te6edf3ByteArrayTff7b72> Tff7b72= Te6edf3_receivedData

Tff7b72private Tff7b72val Te6edf3_meshActivity Tff7b72=
Te6edf3MutableSharedFlowTff7b72<Te6edf3MeshActivityTff7b72>Tb4b4b4(Te6edf3extraBufferCapacity Tff7b72= T79c0ff6T79c0ff4Tb4b4b4, Te6edf3onBufferOverflow Tff7b72= Te6edf3BufferOverflowTb4b4b4.Te6edf3DROP_OLDESTTb4b4b4)
Tff7b72override Tff7b72val Te6edf3meshActivityTb4b4b4: Te6edf3SharedFlowTff7b72<Te6edf3MeshActivityTff7b72> Tff7b72= Te6edf3_meshActivityTb4b4b4.Te6edf3asSharedFlowTb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72val Te6edf3_connectionError Tff7b72= Te6edf3MutableSharedFlowTff7b72<Tffa657StringTff7b72>Tb4b4b4(Te6edf3extraBufferCapacity Tff7b72= T79c0ff6T79c0ff4Tb4b4b4)
Tff7b72override Tff7b72val Te6edf3connectionErrorTb4b4b4: Te6edf3SharedFlowTff7b72<Tffa657StringTff7b72> Tff7b72= Te6edf3_connectionErrorTb4b4b4.Te6edf3asSharedFlowTb4b4b4(Tb4b4b4)

Tff7b72override Tff7b72val Te6edf3serviceScopeTb4b4b4: Te6edf3CoroutineScope
Tff7b72getTb4b4b4(Tb4b4b4) Tff7b72= Te6edf3_serviceScope

Tff7b72private Tff7b72var Te6edf3_serviceScope Tff7b72= Te6edf3CoroutineScopeTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3io Tff7b72+ Te6edf3SupervisorJobTb4b4b4(Tb4b4b4)Tb4b4b4)
Tff7b72private Tff7b72var Te6edf3radioTransportTb4b4b4: Te6edf3RadioTransport? Tff7b72= Tff7b72null
Tff7b72private Tff7b72var Te6edf3runningTransportIdTb4b4b4: Te6edf3InterfaceId? Tff7b72= Tff7b72null
Tff7b72private Tff7b72var Te6edf3isStarted Tff7b72= Tff7b72false

Tff7b72private Tff7b72val Te6edf3listenersInitialized Tff7b72= Te6edf3atomicTb4b4b4(Tff7b72falseTb4b4b4)
Tff7b72private Tff7b72var Te6edf3heartbeatJobTb4b4b4: Te6edf3Job? Tff7b72= Tff7b72null
Tff7b72private Tff7b72var Te6edf3lastHeartbeatMillis Tff7b72= T79c0ff0L

Tf0883e@Volatile Tff7b72private Tff7b72var Te6edf3lastDataReceivedMillis Tff7b72= T79c0ff0L

Tff7b72companion Tff7b72object Tb4b4b4{
Tff7b72private Tff7b72const Tff7b72val Te6edf3HEARTBEAT_INTERVAL_MILLIS Tff7b72= T79c0ff3T79c0ff0 Tff7b72* T79c0ff1T79c0ff0T79c0ff0T79c0ff0L

T8b949e// If we haven't received any data from the radio within this window after sending a
T8b949e// heartbeat while the connection is nominally "Connected", the connection is likely a
T8b949e// zombie (BLE stack didn't report disconnect). Two missed heartbeat intervals gives
T8b949e// the firmware a reasonable window to respond or send telemetry.
Tff7b72private Tff7b72const Tff7b72val Te6edf3LIVENESS_TIMEOUT_MILLIS Tff7b72= Te6edf3HEARTBEAT_INTERVAL_MILLIS Tff7b72* T79c0ff2
Tb4b4b4}

Tff7b72private Tff7b72val Te6edf3initLock Tff7b72= Te6edf3MutexTb4b4b4(Tb4b4b4)
Tff7b72private Tff7b72val Te6edf3transportMutex Tff7b72= Te6edf3MutexTb4b4b4(Tb4b4b4)

Tff7b72private Tff7b72fun Td2a8ffinitStateListenersTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3listenersInitializedTb4b4b4.Te6edf3valueTb4b4b4) Tff7b72return
Te6edf3processLifecycleTb4b4b4.Te6edf3coroutineScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3initLockTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3listenersInitializedTb4b4b4.Te6edf3valueTb4b4b4) Tff7b72returnTf0883e@withLock
Te6edf3listenersInitializedTb4b4b4.Te6edf3value Tff7b72= Tff7b72true

Te6edf3radioPrefsTb4b4b4.Te6edf3devAddr
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3addr Tff7b72-Tff7b72>
Te6edf3transportMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3_currentDeviceAddressFlowTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Te6edf3addrTb4b4b4) Tb4b4b4{
Te6edf3_currentDeviceAddressFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3addr
Te6edf3startTransportLockedTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Te6edf3processLifecycleTb4b4b4.Te6edf3coroutineScopeTb4b4b4)

Te6edf3bluetoothRepositoryTb4b4b4.Te6edf3state
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3state Tff7b72-Tff7b72>
Te6edf3transportMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3stateTb4b4b4.Te6edf3enabledTb4b4b4) Tb4b4b4{
Te6edf3startTransportLockedTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72else Tff7b72if Tb4b4b4(Te6edf3runningTransportId Tff7b72=Tff7b72= Te6edf3InterfaceIdTb4b4b4.Te6edf3BLUETOOTHTb4b4b4) Tb4b4b4{
Te6edf3stopTransportLockedTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffbluetoothRepository.state flow crashedTa5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Te6edf3processLifecycleTb4b4b4.Te6edf3coroutineScopeTb4b4b4)

Te6edf3networkRepositoryTb4b4b4.Te6edf3networkAvailable
Tb4b4b4.Te6edf3onEach Tb4b4b4{ Te6edf3state Tff7b72-Tff7b72>
Te6edf3transportMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3stateTb4b4b4) Tb4b4b4{
Te6edf3startTransportLockedTb4b4b4(Tb4b4b4)
Tb4b4b4} Tff7b72else Tff7b72if Tb4b4b4(Te6edf3runningTransportId Tff7b72=Tff7b72= Te6edf3InterfaceIdTb4b4b4.Te6edf3TCPTb4b4b4) Tb4b4b4{
Te6edf3stopTransportLockedTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}
Tb4b4b4.Te6edf3catch Tb4b4b4{ Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffnetworkRepository.networkAvailable flow crashedTa5d6ff" Tb4b4b4} Tb4b4b4}
Tb4b4b4.Te6edf3launchInTb4b4b4(Te6edf3processLifecycleTb4b4b4.Te6edf3coroutineScopeTb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffconnectTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3processLifecycleTb4b4b4.Te6edf3coroutineScopeTb4b4b4.Te6edf3launch Tb4b4b4{ Te6edf3transportMutexTb4b4b4.Te6edf3withLock Tb4b4b4{ Te6edf3startTransportLockedTb4b4b4(Tb4b4b4) Tb4b4b4} Tb4b4b4}
Te6edf3initStateListenersTb4b4b4(Tb4b4b4)
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffisMockTransportTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657Boolean Tff7b72= Te6edf3transportFactoryTb4b4b4.Te6edf3isMockTransportTb4b4b4(Tb4b4b4)

Tff7b72override Tff7b72fun Td2a8fftoInterfaceAddressTb4b4b4(Te6edf3interfaceIdTb4b4b4: Te6edf3InterfaceIdTb4b4b4, Te6edf3restTb4b4b4: Tffa657StringTb4b4b4)Tb4b4b4: Tffa657String Tff7b72=
Te6edf3transportFactoryTb4b4b4.Te6edf3toInterfaceAddressTb4b4b4(Te6edf3interfaceIdTb4b4b4, Te6edf3restTb4b4b4)

Tff7b72override Tff7b72fun Td2a8ffgetDeviceAddressTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657String? Tff7b72= Te6edf3_currentDeviceAddressFlowTb4b4b4.Te6edf3value

Tff7b72private Tff7b72fun Td2a8ffgetBondedDeviceAddressTb4b4b4(Tb4b4b4)Tb4b4b4: Tffa657String? Tb4b4b4{
Tff7b72val Te6edf3address Tff7b72= Te6edf3getDeviceAddressTb4b4b4(Tb4b4b4)
Tff7b72return Tff7b72if Tb4b4b4(Te6edf3transportFactoryTb4b4b4.Te6edf3isAddressValidTb4b4b4(Te6edf3addressTb4b4b4)Tb4b4b4) Tb4b4b4{
Te6edf3address
Tb4b4b4} Tff7b72else Tb4b4b4{
Tff7b72null
Tb4b4b4}
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffsetDeviceAddressTb4b4b4(Te6edf3deviceAddrTb4b4b4: Tffa657String?Tb4b4b4)Tb4b4b4: Tffa657Boolean Tb4b4b4{
Tff7b72val Te6edf3sanitized Tff7b72= Tff7b72if Tb4b4b4(Te6edf3deviceAddr Tff7b72=Tff7b72= Ta5d6ff"Ta5d6ffnTa5d6ff" Tff7b72|Tff7b72| Te6edf3deviceAddrTb4b4b4.Te6edf3isNullOrBlankTb4b4b4(Tb4b4b4)Tb4b4b4) Tff7b72null Tff7b72else Te6edf3deviceAddr

Tff7b72if Tb4b4b4(Te6edf3getBondedDeviceAddressTb4b4b4(Tb4b4b4) Tff7b72=Tff7b72= Te6edf3sanitized Tff7b72&Tff7b72& Te6edf3isStarted Tff7b72&Tff7b72& Te6edf3_connectionStateTb4b4b4.Te6edf3value Tff7b72=Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffIgnoring setBondedDevice Tffd700${Te6edf3sanitizedTff7b72?.Te6edf3anonymizeTffd700}Ta5d6ff, already using that deviceTa5d6ff" Tb4b4b4}
Tff7b72return Tff7b72false
Tb4b4b4}

Te6edf3analyticsTb4b4b4.Te6edf3trackTb4b4b4(Ta5d6ff"Ta5d6ffmesh_bondTa5d6ff"Tb4b4b4)

Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffSetting bonded device to Tffd700${Te6edf3sanitizedTff7b72?.Te6edf3anonymizeTffd700}Ta5d6ff" Tb4b4b4}
Te6edf3radioPrefsTb4b4b4.Te6edf3setDevAddrTb4b4b4(Te6edf3sanitizedTb4b4b4)
Te6edf3_currentDeviceAddressFlowTb4b4b4.Te6edf3value Tff7b72= Te6edf3sanitized

Te6edf3processLifecycleTb4b4b4.Te6edf3coroutineScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Te6edf3transportMutexTb4b4b4.Te6edf3withLock Tb4b4b4{
Te6edf3ignoreException Tb4b4b4{ Te6edf3stopTransportLockedTb4b4b4(Tb4b4b4) Tb4b4b4}
Te6edf3startTransportLockedTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tff7b72return Tff7b72true
Tb4b4b4}

T8b949e/** Must be called under [transportMutex]. */
Tff7b72private Tff7b72fun Td2a8ffstartTransportLockedTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3radioTransport Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tff7b72return

T8b949e// Never autoconnect to the simulated node. The mock transport may be offered in the
T8b949e// device-picker UI on debug builds, but it must only connect when the user explicitly
T8b949e// selects it (i.e. its address is stored in radioPrefs).
Tff7b72val Te6edf3address Tff7b72= Te6edf3getBondedDeviceAddressTb4b4b4(Tb4b4b4)

Tff7b72if Tb4b4b4(Te6edf3address Tff7b72=Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffNo valid address to connect toTa5d6ff" Tb4b4b4}
Tff7b72return
Tb4b4b4}

Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffStarting radio transport for Tffd700${Te6edf3addressTb4b4b4.Te6edf3anonymizeTffd700}Ta5d6ff" Tb4b4b4}
Te6edf3isStarted Tff7b72= Tff7b72true
Te6edf3runningTransportId Tff7b72= Te6edf3addressTb4b4b4.Te6edf3firstOrNullTb4b4b4(Tb4b4b4)Tff7b72?.Te6edf3let Tb4b4b4{ Te6edf3InterfaceIdTb4b4b4.Te6edf3forIdCharTb4b4b4(Tffa657itTb4b4b4) Tb4b4b4}
Te6edf3radioTransport Tff7b72= Te6edf3transportFactoryTb4b4b4.Te6edf3createTransportTb4b4b4(Te6edf3addressTb4b4b4, Tff7b72thisTb4b4b4)
Te6edf3startHeartbeatTb4b4b4(Tb4b4b4)
Tb4b4b4}

T8b949e/** Must be called under [transportMutex]. */
Tff7b72private Tff7b72fun Td2a8ffstopTransportLockedTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72val Te6edf3currentTransport Tff7b72= Te6edf3radioTransport
Te6edf3LoggerTb4b4b4.Te6edf3i Tb4b4b4{ Ta5d6ff"Ta5d6ffStopping transport Tffd700$Te6edf3currentTransportTa5d6ff" Tb4b4b4}
Te6edf3isStarted Tff7b72= Tff7b72false
Te6edf3radioTransport Tff7b72= Tff7b72null
Te6edf3runningTransportId Tff7b72= Tff7b72null
Te6edf3currentTransportTff7b72?.Te6edf3closeTb4b4b4(Tb4b4b4)

Te6edf3_serviceScopeTb4b4b4.Te6edf3cancelTb4b4b4(Ta5d6ff"Ta5d6ffstopping transportTa5d6ff"Tb4b4b4)
Te6edf3_serviceScope Tff7b72= Te6edf3CoroutineScopeTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3io Tff7b72+ Te6edf3SupervisorJobTb4b4b4(Tb4b4b4)Tb4b4b4)

Tff7b72if Tb4b4b4(Te6edf3currentTransport Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanent Tff7b72= Tff7b72trueTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tff7b72private Tff7b72fun Td2a8ffstartHeartbeatTb4b4b4(Tb4b4b4) Tb4b4b4{
Te6edf3heartbeatJobTff7b72?.Te6edf3cancelTb4b4b4(Tb4b4b4)
Te6edf3lastDataReceivedMillis Tff7b72= Te6edf3nowMillis
Te6edf3heartbeatJob Tff7b72=
Te6edf3serviceScopeTb4b4b4.Te6edf3launch Tb4b4b4{
Tff7b72while Tb4b4b4(Tff7b72trueTb4b4b4) Tb4b4b4{
Te6edf3delayTb4b4b4(Te6edf3HEARTBEAT_INTERVAL_MILLISTb4b4b4)
Te6edf3keepAliveTb4b4b4(Tb4b4b4)
Te6edf3checkLivenessTb4b4b4(Tb4b4b4)
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

T8b949e/**
* Detects zombie connections where the BLE stack didn't report a disconnect.
*
* If we believe we're connected but haven't received any data from the radio within [LIVENESS_TIMEOUT_MILLIS], the
* connection is likely dead. Signal a non-permanent disconnect so the reconnect machinery can take over.
*/
Tff7b72private Tff7b72fun Td2a8ffcheckLivenessTb4b4b4(Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3_connectionStateTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4) Tff7b72return

Tff7b72val Te6edf3silenceMs Tff7b72= Te6edf3nowMillis Tff7b72- Te6edf3lastDataReceivedMillis
Tff7b72if Tb4b4b4(Te6edf3silenceMs Tff7b72> Te6edf3LIVENESS_TIMEOUT_MILLISTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{
Ta5d6ff"Ta5d6ffLiveness check failed: no data received for Tffd700${Te6edf3silenceMsTffd700}Ta5d6ffms Ta5d6ff" Tff7b72+
Ta5d6ff"Ta5d6ff(threshold: Tffd700${Te6edf3LIVENESS_TIMEOUT_MILLISTffd700}Ta5d6ffms). Treating as disconnect.Ta5d6ff"
Tb4b4b4}
Te6edf3onDisconnectTb4b4b4(Te6edf3isPermanent Tff7b72= Tff7b72falseTb4b4b4, Te6edf3errorMessage Tff7b72= Ta5d6ff"Ta5d6ffConnection timeout — no data receivedTa5d6ff"Tb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tff7b72fun Td2a8ffkeepAliveTb4b4b4(Te6edf3nowTb4b4b4: Tffa657Long Tff7b72= Te6edf3nowMillisTb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3now Tff7b72- Te6edf3lastHeartbeatMillis Tff7b72> Te6edf3HEARTBEAT_INTERVAL_MILLISTb4b4b4) Tb4b4b4{
Te6edf3radioTransportTff7b72?.Te6edf3keepAliveTb4b4b4(Tb4b4b4)
Te6edf3lastHeartbeatMillis Tff7b72= Te6edf3now
Tb4b4b4}
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffsendToRadioTb4b4b4(Te6edf3bytesTb4b4b4: Te6edf3ByteArrayTb4b4b4) Tb4b4b4{
T8b949e// Snapshot the transport to avoid calling handleSendToRadio on a null reference.
T8b949e// There is still a benign race: stopTransportLocked() may cancel _serviceScope
T8b949e// between the null-check and the launch, causing the coroutine to be silently
T8b949e// dropped. This is acceptable — if the transport is shutting down, dropping the
T8b949e// send is the correct behavior.
Tff7b72val Te6edf3currentTransport Tff7b72=
Te6edf3radioTransport
Tff7b72?: Te6edf3run Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3w Tb4b4b4{ Ta5d6ff"Ta5d6ffsendToRadio: no active radio transport, dropping Tffd700${Te6edf3bytesTb4b4b4.Te6edf3sizeTffd700}Ta5d6ff bytesTa5d6ff" Tb4b4b4}
Tff7b72return
Tb4b4b4}
Te6edf3_serviceScopeTb4b4b4.Te6edf3handledLaunch Tb4b4b4{
Te6edf3currentTransportTb4b4b4.Te6edf3handleSendToRadioTb4b4b4(Te6edf3bytesTb4b4b4)
Te6edf3_meshActivityTb4b4b4.Te6edf3tryEmitTb4b4b4(Te6edf3MeshActivityTb4b4b4.Te6edf3SendTb4b4b4)
Tb4b4b4}
Tb4b4b4}

Tf0883e@SuppressTb4b4b4(Ta5d6ff"Ta5d6ffTooGenericExceptionCaughtTa5d6ff"Tb4b4b4)
Tff7b72override Tff7b72fun Td2a8ffhandleFromRadioTb4b4b4(Te6edf3bytesTb4b4b4: Te6edf3ByteArrayTb4b4b4) Tb4b4b4{
Tff7b72try Tb4b4b4{
Te6edf3lastDataReceivedMillis Tff7b72= Te6edf3nowMillis
Te6edf3processLifecycleTb4b4b4.Te6edf3coroutineScopeTb4b4b4.Te6edf3launchTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3ioTb4b4b4) Tb4b4b4{ Te6edf3_receivedDataTb4b4b4.Te6edf3emitTb4b4b4(Te6edf3bytesTb4b4b4) Tb4b4b4}
Te6edf3_meshActivityTb4b4b4.Te6edf3tryEmitTb4b4b4(Te6edf3MeshActivityTb4b4b4.Te6edf3ReceiveTb4b4b4)
Tb4b4b4} Tff7b72catch Tb4b4b4(Te6edf3tTb4b4b4: Te6edf3ThrowableTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3eTb4b4b4(Te6edf3tTb4b4b4) Tb4b4b4{ Ta5d6ff"Ta5d6ffhandleFromRadio failed while emitting dataTa5d6ff" Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffonConnectTb4b4b4(Tb4b4b4) Tb4b4b4{
T8b949e// MutableStateFlow.value is thread-safe (backed by atomics) — assign directly rather than
T8b949e// launching a coroutine. The async launch pattern introduced a window where a concurrent
T8b949e// onDisconnect launch could execute AFTER an onConnect launch, leaving the service stuck
T8b949e// in Connected while the transport was actually disconnected.
Te6edf3lastDataReceivedMillis Tff7b72= Te6edf3nowMillis
Tff7b72if Tb4b4b4(Te6edf3_connectionStateTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3ConnectedTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffBroadcasting connection state change to ConnectedTa5d6ff" Tb4b4b4}
Te6edf3_connectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3ConnectionStateTb4b4b4.Te6edf3Connected
Tb4b4b4}
Tb4b4b4}

Tff7b72override Tff7b72fun Td2a8ffonDisconnectTb4b4b4(Te6edf3isPermanentTb4b4b4: Tffa657BooleanTb4b4b4, Te6edf3errorMessageTb4b4b4: Tffa657String?Tb4b4b4) Tb4b4b4{
Tff7b72if Tb4b4b4(Te6edf3errorMessage Tff7b72!Tff7b72= Tff7b72nullTb4b4b4) Tb4b4b4{
Te6edf3processLifecycleTb4b4b4.Te6edf3coroutineScopeTb4b4b4.Te6edf3launchTb4b4b4(Te6edf3dispatchersTb4b4b4.Te6edf3defaultTb4b4b4) Tb4b4b4{ Te6edf3_connectionErrorTb4b4b4.Te6edf3emitTb4b4b4(Te6edf3errorMessageTb4b4b4) Tb4b4b4}
Tb4b4b4}
Tff7b72val Te6edf3newTargetState Tff7b72= Tff7b72if Tb4b4b4(Te6edf3isPermanentTb4b4b4) Te6edf3ConnectionStateTb4b4b4.Te6edf3Disconnected Tff7b72else Te6edf3ConnectionStateTb4b4b4.Te6edf3DeviceSleep
Tff7b72if Tb4b4b4(Te6edf3_connectionStateTb4b4b4.Te6edf3value Tff7b72!Tff7b72= Te6edf3newTargetStateTb4b4b4) Tb4b4b4{
Te6edf3LoggerTb4b4b4.Te6edf3d Tb4b4b4{ Ta5d6ff"Ta5d6ffBroadcasting connection state change to Tffd700$Te6edf3newTargetStateTa5d6ff" Tb4b4b4}
Te6edf3_connectionStateTb4b4b4.Te6edf3value Tff7b72= Te6edf3newTargetState
Tb4b4b4}
Tb4b4b4}
Tb4b4b4}

Served by rngit 1.5.0 - Generated in 0.08s